import {
	AfterContentInit,
	Attribute,
	ChangeDetectorRef,
	ContentChild,
	ContentChildren,
	DestroyRef,
	Directive,
	ElementRef,
	EventEmitter,
	forwardRef,
	inject,
	Input,
	OnChanges,
	OnInit,
	Output,
	QueryList,
	SimpleChanges,
	TemplateRef,
} from '@angular/core';
import { DOCUMENT } from '@angular/common';

import { Subject } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

import { isDefined } from '../util/util';
import { NgbNavConfig } from './nav-config';

const isValidNavId = (id: any) => isDefined(id) && id !== '';

let navCounter = 0;

/**
 * Context passed to the nav content template.
 *
 * See [this demo](#/components/nav/examples#keep-content) as the example.
 *
 * @since 5.2.0
 */
export interface NgbNavContentContext {
	/**
	 * If `true`, current nav content is visible and active
	 */
	$implicit: boolean;
}

/**
 * This directive must be used to wrap content to be displayed in the nav.
 *
 * @since 5.2.0
 */
@Directive({ selector: 'ng-template[ngbNavContent]', standalone: true })
export class NgbNavContent {
	templateRef = inject(TemplateRef);
}

/**
 * This directive applies a specific role on a non-container based ngbNavItem.
 *
 * @since 14.1.0
 */
@Directive({
	selector: '[ngbNavItem]:not(ng-container)',
	standalone: true,
	host: {
		'[attr.role]': `role ? role : nav.roles ? 'presentation' : undefined`,
	},
})
export class NgbNavItemRole {
	nav = inject(NgbNav);

	constructor(@Attribute('role') public role: string) {}
}

/**
 * The directive used to group nav link and related nav content. As well as set nav identifier and some options.
 *
 * @since 5.2.0
 */
@Directive({
	selector: '[ngbNavItem]',
	exportAs: 'ngbNavItem',
	standalone: true,
	host: {
		class: 'nav-item',
	},
})
export class NgbNavItem implements OnInit {
	private _nav = inject(NgbNav);
	private _nativeElement = inject(ElementRef).nativeElement as HTMLElement;

	/**
	 * If `true`, non-active current nav item content will be removed from DOM
	 * Otherwise it will just be hidden
	 */
	@Input() destroyOnHide;

	/**
	 * If `true`, the current nav item is disabled and can't be toggled by user.
	 *
	 * Nevertheless disabled nav can be selected programmatically via the `.select()` method and the `[activeId]` binding.
	 */
	@Input() disabled = false;

	/**
	 * The id used for the DOM elements.
	 * Must be unique inside the document in case you have multiple `ngbNav`s on the page.
	 *
	 * Autogenerated as `ngb-nav-XXX` if not provided.
	 */
	@Input() domId: string;

	/**
	 * The id used as a model for active nav.
	 * It can be anything, but must be unique inside one `ngbNav`.
	 *
	 * The only limitation is that it is not possible to have the `''` (empty string) as id,
	 * because ` ngbNavItem `, `ngbNavItem=''` and `[ngbNavItem]="''"` are indistinguishable
	 */
	@Input('ngbNavItem') _id: any;

	/**
	 * An event emitted when the fade in transition is finished on the related nav content
	 *
	 * @since 8.0.0
	 */
	@Output() shown = new EventEmitter<void>();

	/**
	 * An event emitted when the fade out transition is finished on the related nav content
	 *
	 * @since 8.0.0
	 */
	@Output() hidden = new EventEmitter<void>();

	@ContentChild(NgbNavContent, { descendants: false }) contentTpl?: NgbNavContent;

	ngOnInit() {
		if (!isDefined(this.domId)) {
			this.domId = `ngb-nav-${navCounter++}`;
		}
	}

	get active() {
		return this._nav.activeId === this.id;
	}

	get id() {
		return isValidNavId(this._id) ? this._id : this.domId;
	}

	get panelDomId() {
		return `${this.domId}-panel`;
	}

	isPanelInDom() {
		return (isDefined(this.destroyOnHide) ? !this.destroyOnHide : !this._nav.destroyOnHide) || this.active;
	}

	/**
	 * @internal
	 */
	isNgContainer() {
		return this._nativeElement.nodeType === Node.COMMENT_NODE;
	}
}

/**
 * A nav directive that helps with implementing tabbed navigation components.
 *
 * @since 5.2.0
 */
@Directive({
	selector: '[ngbNav]',
	exportAs: 'ngbNav',
	standalone: true,
	host: {
		class: 'nav',
		'[class.flex-column]': `orientation === 'vertical'`,
		'[attr.aria-orientation]': `orientation === 'vertical' && roles === 'tablist' ? 'vertical' : undefined`,
		'[attr.role]': `role ? role : roles ? 'tablist' : undefined`,
		'(keydown.arrowLeft)': 'onKeyDown($event)',
		'(keydown.arrowRight)': 'onKeyDown($event)',
		'(keydown.arrowDown)': 'onKeyDown($event)',
		'(keydown.arrowUp)': 'onKeyDown($event)',
		'(keydown.Home)': 'onKeyDown($event)',
		'(keydown.End)': 'onKeyDown($event)',
		'(focusout)': 'onFocusout($event)',
	},
})
export class NgbNav implements AfterContentInit, OnChanges {
	static ngAcceptInputType_orientation: string;
	static ngAcceptInputType_roles: boolean | string;

	private _config = inject(NgbNavConfig);
	private _cd = inject(ChangeDetectorRef);
	private _document = inject(DOCUMENT);
	private _nativeElement = inject(ElementRef).nativeElement as HTMLElement;

	destroyRef = inject(DestroyRef);

	_navigatingWithKeyboard = false;

	/**
	 * The id of the nav that should be active
	 *
	 * You could also use the `.select()` method and the `(navChange)` event
	 */
	@Input() activeId: any;

	/**
	 * The event emitted after the active nav changes
	 * The payload of the event is the newly active nav id
	 *
	 * If you want to prevent nav change, you should use `(navChange)` event
	 */
	@Output() activeIdChange = new EventEmitter<any>();

	/**
	 * If `true`, nav change will be animated.
	 *
	 * @since 8.0.0
	 */
	@Input() animation = this._config.animation;

	/**
	 * If `true`, non-active nav content will be removed from DOM
	 * Otherwise it will just be hidden
	 */
	@Input() destroyOnHide = this._config.destroyOnHide;

	/**
	 * The orientation of navs.
	 *
	 * Using `vertical` will also add the `aria-orientation` attribute
	 */
	@Input() orientation = this._config.orientation;

	/**
	 * Role attribute generating strategy:
	 * - `false` - no role attributes will be generated
	 * - `'tablist'` - 'tablist', 'tab' and 'tabpanel' will be generated (default)
	 */
	@Input() roles = this._config.roles;

	/**
	 * Keyboard support for nav focus/selection using arrow keys.
	 *
	 * * `true` - navs will be focused using keyboard arrow keys
	 * * `false` - no keyboard support
	 * * `'changeWithArrows'` -  nav will be selected using keyboard arrow keys
	 *
	 * See the [list of available keyboard shortcuts](#/components/nav/overview#keyboard-shortcuts).
	 *
	 * @since 6.1.0
	 */
	@Input() keyboard = this._config.keyboard;

	/**
	 * An event emitted when the fade in transition is finished for one of the items.
	 *
	 * Payload of the event is the nav id that was just shown.
	 *
	 * @since 8.0.0
	 */
	@Output() shown = new EventEmitter<any>();

	/**
	 * An event emitted when the fade out transition is finished for one of the items.
	 *
	 * Payload of the event is the nav id that was just hidden.
	 *
	 * @since 8.0.0
	 */
	@Output() hidden = new EventEmitter<any>();

	@ContentChildren(NgbNavItem) items: QueryList<NgbNavItem>;
	@ContentChildren(forwardRef(() => NgbNavLinkBase), { descendants: true }) links: QueryList<NgbNavLinkBase>;

	navItemChange$ = new Subject<NgbNavItem | null>();

	constructor(@Attribute('role') public role: string) {}

	/**
	 * The nav change event emitted right before the nav change happens on user click.
	 *
	 * This event won't be emitted if nav is changed programmatically via `[activeId]` or `.select()`.
	 *
	 * See [`NgbNavChangeEvent`](#/components/nav/api#NgbNavChangeEvent) for payload details.
	 */
	@Output() navChange = new EventEmitter<NgbNavChangeEvent>();

	click(item: NgbNavItem) {
		if (!item.disabled) {
			this._updateActiveId(item.id);
		}
	}

	onFocusout({ relatedTarget }: FocusEvent) {
		if (!this._nativeElement.contains(relatedTarget as HTMLElement)) {
			this._navigatingWithKeyboard = false;
		}
	}

	onKeyDown(event: KeyboardEvent) {
		if (this.roles !== 'tablist' || !this.keyboard) {
			return;
		}
		const enabledLinks = this.links.filter((link) => !link.navItem.disabled);
		const { length } = enabledLinks;

		let position = -1;

		enabledLinks.forEach((link, index) => {
			if (link.nativeElement === this._document.activeElement) {
				position = index;
			}
		});

		if (length) {
			switch (event.key) {
				case 'ArrowUp':
				case 'ArrowLeft':
					position = (position - 1 + length) % length;
					break;
				case 'ArrowRight':
				case 'ArrowDown':
					position = (position + 1) % length;
					break;
				case 'Home':
					position = 0;
					break;
				case 'End':
					position = length - 1;
					break;
			}
			if (this.keyboard === 'changeWithArrows') {
				this.select(enabledLinks[position].navItem.id);
			}
			enabledLinks[position].nativeElement.focus();
			this._navigatingWithKeyboard = true;

			event.preventDefault();
		}
	}

	/**
	 * Selects the nav with the given id and shows its associated pane.
	 * Any other nav that was previously selected becomes unselected and its associated pane is hidden.
	 */
	select(id: any) {
		this._updateActiveId(id, false);
	}

	ngAfterContentInit() {
		if (!isDefined(this.activeId)) {
			const nextId = this.items.first ? this.items.first.id : null;
			if (isValidNavId(nextId)) {
				this._updateActiveId(nextId, false);
				this._cd.detectChanges();
			}
		}

		this.items.changes
			.pipe(takeUntilDestroyed(this.destroyRef))
			.subscribe(() => this._notifyItemChanged(this.activeId));
	}

	ngOnChanges({ activeId }: SimpleChanges): void {
		if (activeId && !activeId.firstChange) {
			this._notifyItemChanged(activeId.currentValue);
		}
	}

	private _updateActiveId(nextId: any, emitNavChange = true) {
		if (this.activeId !== nextId) {
			let defaultPrevented = false;

			if (emitNavChange) {
				this.navChange.emit({
					activeId: this.activeId,
					nextId,
					preventDefault: () => {
						defaultPrevented = true;
					},
				});
			}

			if (!defaultPrevented) {
				this.activeId = nextId;
				this.activeIdChange.emit(nextId);
				this._notifyItemChanged(nextId);
			}
		}
	}

	private _notifyItemChanged(nextItemId: any) {
		this.navItemChange$.next(this._getItemById(nextItemId));
	}

	private _getItemById(itemId: any): NgbNavItem | null {
		return (this.items && this.items.find((item) => item.id === itemId)) || null;
	}
}

@Directive({
	selector: '[ngbNavLink]',
	standalone: true,
	host: {
		'[id]': 'navItem.domId',
		class: 'nav-link',
		'[class.nav-item]': 'navItem.isNgContainer()',
		'[attr.role]': `role ? role : nav.roles ? 'tab' : undefined`,
		'[class.active]': 'navItem.active',
		'[class.disabled]': 'navItem.disabled',
		'[attr.tabindex]': 'tabindex',
		'[attr.aria-controls]': 'navItem.isPanelInDom() ? navItem.panelDomId : null',
		'[attr.aria-selected]': 'navItem.active',
		'[attr.aria-disabled]': 'navItem.disabled',
	},
})
export class NgbNavLinkBase {
	navItem = inject(NgbNavItem);
	nav = inject(NgbNav);
	nativeElement = inject(ElementRef).nativeElement as HTMLElement;

	constructor(@Attribute('role') public role: string) {}

	get tabindex() {
		if (this.nav.keyboard === false) {
			return this.navItem.disabled ? -1 : undefined;
		}
		if (this.nav._navigatingWithKeyboard) {
			return -1;
		}
		return this.navItem.disabled || !this.navItem.active ? -1 : undefined;
	}
}

/**
 * A directive to mark the nav link when used on a button element.
 */
@Directive({
	selector: 'button[ngbNavLink]',
	standalone: true,
	host: {
		type: 'button',
		'[disabled]': 'navItem.disabled',
		'(click)': 'nav.click(navItem)',
	},
})
export class NgbNavLinkButton extends NgbNavLinkBase {}

/**
 * A directive to mark the nav link when used on a link element.
 *
 * @since 5.2.0
 */
@Directive({
	selector: 'a[ngbNavLink]',
	standalone: true,
	host: {
		href: '',
		'(click)': 'nav.click(navItem); $event.preventDefault()',
	},
})
export class NgbNavLink extends NgbNavLinkBase {}

/**
 * The payload of the change event emitted right before the nav change happens on user click.
 *
 * This event won't be emitted if nav is changed programmatically via `[activeId]` or `.select()`.
 *
 * @since 5.2.0
 */
export interface NgbNavChangeEvent<T = any> {
	/**
	 * Id of the currently active nav.
	 */
	activeId: T;

	/**
	 * Id of the newly selected nav.
	 */
	nextId: T;

	/**
	 * Function that will prevent nav change if called.
	 */
	preventDefault: () => void;
}
